#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <getopt.h>
#include <math.h>
#include "parser.h"
#include "svdutil.h"
#include "svdlib.h"
#include "dir_explorer.h"
#include "utilidades.h"
#include "vector_dinamico.h"
#include "matriz_svdlibc.h"
#include "heap.h"
#include "lista.h"
#include "abb_avl.h"
#include "termino.h"
#include "manejo_de_indices.h"


/* *********************************************************** *
 *		    DECLARACION DE OPCIONES 		       *
 * *********************************************************** */

#define QUE 'q'
#define CANT_DOCS 'n'
#define TERM 'p'
#define REPO 'r'
#define HELP 'h'
#define STEP 's'


/* *********************************************************** *
 *	    DECLARACION DE CONSTANTES DEL PROGRAMA 	       *
 * *********************************************************** */

#define INDEXFILE "file-id"
#define UxS "-UxS"
#define Vt "-Vt"
#define QVECT "q-vect.mat"
#define NTERMINOS "filas.log"
#define RANKING "rank.mat"
#define DOCUMENTS "file-id"
#define PUNTEROS "term_index.bin"
#define LEXICO "term_string"


/* *********************************************************** *
 * 		FUNCIONES AUXILIARES DEL PROGRAMA              *
 * *********************************************************** */

//Imprime por pantalla un aviso de error fatal.
void fatal_error(char* msj){

	fprintf (stderr,"%s \nEl programa se cerrara...\n",msj);
	exit(-1);

}


FILE* path_file_open(char* dir, char* filename, char* mode){

	char* path = componer_ruta(dir, filename);
	if (!path)
		return NULL;

	FILE* fd = fopen(path,mode);

	free(path);

	return fd;

}


//Impresion por pantalla del comando --help.
void print_help(char* prog_name){

	printf("Modo de empleo: %s [OPCIONES]...\n",prog_name);
	printf("Realiza una busqueda ranqueada de la consulta sobre el repositorio pasado como parametro\n");
	printf("\nLos argumentos obligatorios para las opciones largas son también obligatorios para las opciones cortas.\n");
	printf("-q | --query=\"consulta\" 			Terminos a buscar\n");
	printf("-n | --cant-doc=<num>			Cantidad de documentos que seran mostrados en el ranqueo. (Default, 0 - todos los documentos)\n");
	printf("-p | --palabras-x-termino=<num>		Establece la cantidad de palabras que pueden formar un termino de indexacion. Por defecto, se establece en 3 palabras\n");
	printf("-r | --repositorio=<dir>		Directorio en el que se encuentran los archivos resultantes de la indexacion de los documentos\n");
	printf("-h | --help				Muestra el mensaje de ayuda del programa\n");
	printf("\nDesarrollado por Gonzalo Beviglia, Lucas Simonelli y Tomás Boccardo.\nOrganizacion de Datos 75.06, 2do Cuatrimestre 2012\n");
	exit(0);
}

int comparar_clave(char* clave1, char* clave2){
	//Convertimos las claves a numero
	unsigned long int nro1 = strtol(clave1,NULL,10);
	unsigned long int nro2 = strtol(clave2,NULL,10);
	
	if(nro1>nro2) return 1;
	else if(nro2>nro1) return -1;
	
	return 0;
	
}

void wrapper_destr_termino (void* termino){
	termino= (termino_t*)termino;
	termino_destruir(termino);
}


/* *********************************************************** *
 * ****************  TIPO DE DATOS DOCUMENTO  **************** *
 * *********************************************************** */

// Estructura que representa un documento y su similitud del coseno
// con el vector query.
typedef struct document{

	int id;
	char* path;
	double value;

}document_t;


document_t* document_new(int id, char* path, double value){

	document_t* doc = malloc(sizeof(document_t));
	if (!doc)
		return NULL;

	doc->id = id;
	doc->path = calloc(strlen(path)+1,sizeof(char));
	if (!doc->path){
		free(doc);
		return NULL;
	}

	strcpy(doc->path,path);
	doc->value = value;

	return doc;
}


void document_destroy(document_t* doc){

	free(doc->path);
	free(doc);

}


//Funcion wrapper para document_destroy
void wrapper_document_destroy(void* doc){

	document_destroy(doc);

}


// Funcion de comparacion entre dos documentos.
int cmp_documents(const document_t* doc1,const document_t* doc2){

	if ((doc1->value)<(doc2->value))
		return -1;

	if ((doc1->value)>(doc2->value))
		return 1;

	return 0;

}


// Norma del vector recibido. Necesita la dimension del mismo.
double vector_norm(double* vector, int dim){
        
        double norm = 0;
        
        int i;
        
        for( i=0 ; i<dim ; i++ )
                
                norm+=(vector[i]*vector[i]);
                
        norm = sqrt(norm);
        
        return norm;
        
}


// Producto escalar entre vectores de igual dimension.
double vector_scalar_product(double* vec1, double* vec2, int dim){
        
        int i;
        
        double product_value = 0;
        
        for( i=0 ; i<dim ; i++ )
        
                product_value += vec1[i]*vec2[i];
                
        return product_value;
        
}


typedef struct wrapper_query_reducido{
	
	double* vec;
	
	unsigned long int dim;
	
}query_r;


/* ************************************************************* *
 * ****************  	METODO DEL COSENO 	**************** *
 * ************************************************************* */

// Devuelve un heap con los documentos mas relevantes respecto de la similitud
// del coseno entre cada documento y el vector query.
heap_t* cosine_similarities(query_r* vectQ, char* repo){
        
        int dimension = (vectQ->dim);
        double* vectQValues = vectQ->vec;
        
        // Heap creation and compare function definition.
        cmp_func_t cmp = (cmp_func_t) cmp_documents;
        heap_t* documents = heap_crear(cmp);
        
        // Dense matrix opening. Document matrix.
        int readFormat = SVD_F_DB;
        char* matriz = componer_ruta(repo,Vt);
        DMat VT = svdLoadDenseMatrix(matriz, readFormat);
        int rows = VT->rows;
        free(matriz);
        
        // Calculate query vector norm.
        double query_norm = vector_norm(vectQValues,dimension);
        
        // Auxiliary variables.
        int i;
        double* actual_document_vector = NULL;
        double actual_cosine_value = 0;
        document_t* actual_document = NULL;
        
        char* path = componer_ruta(repo,DOCUMENTS);
		FILE* docs = fopen(path,"r");
		free(path);
		char* ruta = NULL;
        
        // Perform cosine similarities with all documents.
        for( i=0 ; i<rows ; i++ ){
                
                
                // Obtains next document vector.
                actual_document_vector = (VT->value)[i];
                
                // Calculates cosine value.
                actual_cosine_value = vector_scalar_product(vectQValues,actual_document_vector,dimension);
                actual_cosine_value = actual_cosine_value / vector_norm(actual_document_vector,dimension);
                actual_cosine_value = actual_cosine_value / query_norm;
                
                // Verification for Zero Division in Vector Norm.
                if( isnan(actual_cosine_value) )actual_cosine_value = -1;
                
                // Creates new document entry.
                actual_document = (document_t*) malloc( sizeof(document_t) );
                actual_document->id = i;
                actual_document->value = actual_cosine_value;
                ruta = leer_linea(docs);
                actual_document->path = ruta;
                
                // Stores document in heap.
                heap_encolar(documents,actual_document);
                
        }
        
        svdFreeDMat(VT);
        fclose(docs);
        
        // Returns the heap.
        return documents;
        
}

/* *********************************************************** *
 * ****************  FUNCIONES PRINCIPALES ******************* *
 * *********************************************************** */

// Extrae los parametros pasados al programa
bool extraer_opciones(int argc, char** argv, char** query, int* cant ,int* term ,char** repo,int* steps) {

	int option_index = 0;

	/* struct para el uso interno del getopt long */
	static struct option long_options[] = {

			{ "query", required_argument, NULL, QUE },
			{ "cant-doc", required_argument, NULL, CANT_DOCS },
			{ "reposiorio", required_argument, NULL, REPO },
			{ "palabras-x-termino", required_argument, NULL, TERM },
			{ "help",no_argument, NULL, HELP },
			{ "step",required_argument, NULL, STEP },
			{ 0, 0, 0, 0 }

	};

	int option = 0;

	/* Mientras getopt_long lea parametros... */
	while ((option = getopt_long(argc, argv, "q:n:p:r:h:s", long_options, &option_index)) != -1) {

		/* Veo que parametro corresponde y cambio los valores de las variables correspondientes */
		switch (option) {
			case QUE:
				*query = optarg;
				break;
			case CANT_DOCS:
				*cant = atoi(optarg);
				break;
			case TERM:
				*term = atoi(optarg);
				break;
			case REPO:
				*repo = optarg;
				break;
			case HELP:
				print_help(argv[0]);
				break;
			case STEP:
				*steps = atoi(optarg);
				break;
			default:
				printf("Opcion no reconocida\n");
				print_help(argv[0]);
				break;
	}

	}

	return true;

}

//Realiza un listado de los ducumentos ordenados por relevancia
document_t* realizar_reporte(heap_t* rankeo, int cant){

	int i = 0;
	
	document_t* mas_relevante = NULL;
	
	printf("Ranking\t-\tRelevancia\t-\tDocumento\n\n");

	while (! heap_esta_vacio(rankeo)){

		// Si me pasaron una cantidad maxima de documentos a mostrar
		// debo controlar para no imprimir mas de los que me pidieron.
		if ( cant != 0 && i >= cant ) heap_desencolar(rankeo);
		
		else{
			
			document_t* actual = heap_desencolar(rankeo);
			
			// Se imprime la informacion del documento actual.
			printf("%d\t-\t%.3f\t\t-\t%s\n",i+1,actual->value,actual->path);
			
			if( i==0 ) mas_relevante = document_new(1,actual->path,actual->value);
			
			document_destroy(actual);
			
		}

		i++;

	}
	
	return mas_relevante;

}

/* **************************************************************** *
 * ****************  OBTENCION DEL VECTOR QUERY ******************* *
 * **************************************************************** */

//Devuelve un AVL con formato {indice en vector: repeticiones del termino en query}
abb_avl_t* obtener_vector_consulta(abb_avl_t* avl_terminos,FILE* punteros, FILE* lexico, unsigned long int largo_punteros_file){
	abb_avl_t* avl = avl_crear((abb_comparar_clave_t)comparar_clave,free);
	if (!avl) fatal_error("ERROR: Se acabo la  memoria al obtener vector consulta");
	
	avl_iter_t* iter = avl_iter_in_crear(avl_terminos);
	
	int long_reg = sizeof(unsigned long int);
	
	//Obtenemos los indices de cada termino y los cargamos al AVL
	while(!avl_iter_in_al_final(iter)){
		char* actual = (char*)avl_iter_in_ver_actual(iter);
		unsigned long int indice = busqueda_binaria_en_archivo(actual,0,largo_punteros_file-long_reg,punteros,lexico);
		
		//Si el termino fue indexado
		if (indice!=-1){
			char posicion [30];
			sprintf(posicion,"%ld",indice);
			termino_t* term = (termino_t*)avl_obtener(avl_terminos,actual);
			int* cantidad = malloc(sizeof(int));
			*cantidad = *(term->repeticiones);
			avl_guardar(avl,posicion,cantidad);
		}
		avl_iter_in_avanzar(iter);
		
	}
	
	avl_iter_in_destruir(iter);
	return avl;
}

//Pasa la lista de terminos al AVL. Los casos repetidos se manejan incrementando repeticiones
void pasar_list_a_avl(lista_t* lista_terminos,abb_avl_t* avl){
	
	lista_iter_t* iter = lista_iter_crear(lista_terminos);

	while(!lista_iter_al_final(iter)){
		termino_t* term=(termino_t*)lista_iter_ver_actual(iter) ;
		
		termino_t* valor = (termino_t*)avl_obtener(avl,term->valor);
		
		if (!valor){
				//Si no esta.
				*(term->repeticiones)=*(term->repeticiones) +1;
				avl_guardar(avl,term->valor,term);
			}
		else{ 
			*(valor->repeticiones)=*(valor->repeticiones) +1;
			termino_destruir(term);
		}
		lista_iter_avanzar(iter);	
	}
	
	lista_iter_destruir(iter);

}

// Multiplica el vector vec por la matriz densa mat. Escribe el resultado en el vector dest.
// Recibe la dimension del vector vec.
void vector_por_matriz(double* vec,DMat mat,double* dest,unsigned long int dim_vec){
	
	int vecIndex;
	int colIndex;
	int cols = mat->cols;
	
	for( colIndex = 0 ; colIndex < cols ; colIndex++ ){
	
		dest[colIndex] = 0;
		
		for( vecIndex = 0 ; vecIndex < dim_vec ; vecIndex++ )
			
			dest[colIndex] += vec[vecIndex] * (mat->value[vecIndex][colIndex]);
			
	}
	
}

// Divide cada elemento del vector recibido por la norma del vector.
// Recibe el vector y la dimension del vector.
void normalizar_vector(double* vec, unsigned long int dim){
	
	double norma = 0;
	
	int i;
	
	for( i=0 ; i<dim ; i++ ) norma += pow(vec[i],2);
	
	norma = sqrt(norma);
	
	for( i=0 ; i<dim ; i++ ) vec[i] = vec[i]/norma;
	
}

// Obtiene las coordenadas del vector query en las nuevas dimensiones.
query_r* obtener_vector_query_reducido(abb_avl_t* vector, char* repo){
	
	// Obtengo la cantidad total de terminos.
	char* ruta_terms = componer_ruta(repo,NTERMINOS);
	FILE* term_file = fopen(ruta_terms,"r");
	free(ruta_terms);
	unsigned long int num_terms = 0;
	fscanf(term_file, "%lu", &num_terms);
	fclose(term_file);
	
	// Inicializo el vector query completo, con todos sus elementos en 0.
	double* q_completo = (double*) malloc( num_terms*sizeof(double) );
	for( int i = 0 ; i < num_terms ; i++ ) q_completo[i] = 0;
	
	// Cargo la matriz U * S^-1
	char* rutaUxS = componer_ruta(repo,UxS);
	DMat UxS_Inversa = svdLoadDenseMatrix(rutaUxS,SVD_F_DB);
	free(rutaUxS);
	
	// q = Qt * U * S^-1
	double* q_reducido = (double*) malloc( (UxS_Inversa->cols) * sizeof(double) );
	
	avl_iter_t* iter = avl_iter_in_crear(vector);
	
	// Cargo los valores del vector query completo.
	while ( !avl_iter_in_al_final(iter) ){
		
		const char* clave = avl_iter_in_ver_actual(iter);
		unsigned long int index = strtoul(clave,NULL,10);

		int cantidad = *( (int*)avl_obtener(vector,clave) );
		
		q_completo[index] = cantidad;

		avl_iter_in_avanzar(iter);
		
	}
	
	avl_iter_in_destruir(iter);
	
	// Obtengo nuevas coordenadas del vector query. q = Q * U * S^-1
	vector_por_matriz(q_completo,UxS_Inversa,q_reducido,num_terms);
	
	// Normalizo el vector de forma que ||q|| = 1.
	normalizar_vector(q_reducido,UxS_Inversa->cols);
	
	// Packeo el resultado en un vector query_r*
	query_r* query_reducido = (query_r*) malloc( sizeof(query_r) );
	query_reducido -> vec = q_reducido;
	query_reducido -> dim = UxS_Inversa->cols;
	
	// Libero memoria.
	free(q_completo);
	svdFreeDMat(UxS_Inversa);
	
	return query_reducido;
	
}

void abrir_documento(document_t* doc){
	
	char c = 0;
	while (c != 's' && c != 'n' && c != 'S' && c != 'N'){
		printf ("\nDesea abrir el archivo mas relevante? (S/N): ");
		c = getchar();
		getchar();
	}
	
	if (c == 'n' || c == 'N') return;
	printf("\n");
	
	while (c != '1' && c != '2' && c != '3'){
		printf ("Abrir con: \n1 - vi\n2 - gedit\n3 - geany\n");
		c = getchar();
		getchar();
	}
	
	char* programa = NULL;
	
	switch(c){
		
		case '1':
			programa = malloc( strlen("vim")*sizeof(char) );
			sprintf(programa,"%s","vim");
			break;
			
		case '2':
			programa = malloc( strlen("gedit")*sizeof(char) );
			sprintf(programa,"%s","gedit");
			break;
			
		case '3':
			programa = malloc( strlen("geany")*sizeof(char) );
			sprintf(programa,"%s","geany");
			break;
		
	}
	
	
	size_t comando_size = 1 + strlen(doc->path) + strlen(programa);
	
	char* comando = (char*) malloc( comando_size * sizeof(char) );
	
	sprintf(comando,"%s \"%s\"",programa,doc->path);
	
	document_destroy(doc);
	
	printf("\nEjecutando: %s\n",comando);
	system(comando);
	free(comando);
	
}

/* *********************************************************** *
 * ***********************  MAIN ***************************** *
 * *********************************************************** */

int main (int argc,char** argv){

	/* Variables para guardar los resultados, incializadas en la manera
	 * que se comportan por default. */

	char* query = NULL;
	char* repo = "../indexer";
	int cant = 0;
	int term = 3;
	int steps = 0;

	// Se extraen las opciones pasadas como parametro al programa.
	extraer_opciones(argc,argv,&query,&cant,&term,&repo,&steps);

	// Se controla de que haya sido introducida una consulta.
	if (!query) fatal_error("ERROR: No se introdujo la consulta");
	
	FILE* punteros_file = path_file_open(repo, PUNTEROS,"rb");
	FILE* lexico_file = path_file_open(repo,LEXICO,"r");
	
	if (!punteros_file || !lexico_file) fatal_error("ERROR: NO SE ENCUENTRAN LOS INDICES");
	
	//Buscamos el tamaño del archivo de punteros
	fseek(punteros_file,0,SEEK_END);
	unsigned long int largo_punteros = ftell(punteros_file);
	
	// Se muestra la consulta realizada por pantalla.
	printf("\nCONSULTA: \"%s\"\n\n",query);

	// Separamos la consulta en terminos.
	lista_t* terminos = separar_cadena_en_terminos(query,term,NULL);

	// Necesitamos ordenar los terminos, usamos un avl; paja incluir el heap en el makefile, etc.
	abb_avl_t* avl_terminos_query = avl_crear(strcmp,wrapper_destr_termino);
	pasar_list_a_avl(terminos,avl_terminos_query);
	lista_destruir(terminos, NULL);
	
	// Buscamos los terminos en el archivo de lexico y obtenemos el vector query [SPARSE]
	abb_avl_t* vector_query = obtener_vector_consulta(avl_terminos_query,punteros_file,lexico_file,largo_punteros);
	avl_destruir(avl_terminos_query);
	
	// Se busca un termino que no estaba en ningun documento.
	if( avl_cantidad(vector_query) == 0 ) fatal_error("Su consulta no ha sido indexada, intente con otra");
	
	// Guardo el vector query en formato octave
	query_r* vector_query_reducido = obtener_vector_query_reducido(vector_query,repo);
	avl_destruir(vector_query);

	// Obtengo el ranqueo de los documentos
	heap_t* rankeo = cosine_similarities(vector_query_reducido,repo);

	// Imprimo por pantalla el ranking
	document_t* mas_relevante = NULL;
	mas_relevante = realizar_reporte(rankeo,cant);
	
	abrir_documento(mas_relevante);
	
	// Libero memoria.
	heap_destruir(rankeo,wrapper_document_destroy);
	free(vector_query_reducido->vec);
	free(vector_query_reducido);
	fclose(lexico_file);
	fclose(punteros_file);

	return 0;

}
